// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/events/gesture_detection/gesture_provider.h"

#include <stddef.h>

#include <cmath>

#include "base/auto_reset.h"
#include "base/macros.h"
#include "base/trace_event/trace_event.h"
#include "ui/events/event_constants.h"
#include "ui/events/gesture_detection/gesture_event_data.h"
#include "ui/events/gesture_detection/gesture_listeners.h"
#include "ui/events/gesture_detection/motion_event.h"
#include "ui/events/gesture_detection/motion_event_generic.h"
#include "ui/events/gesture_detection/scale_gesture_listeners.h"
#include "ui/gfx/geometry/point_f.h"

namespace ui {
namespace {

    // Double-tap drag zoom sensitivity (speed).
    const float kDoubleTapDragZoomSpeed = 0.005f;

    const char* GetMotionEventActionName(MotionEvent::Action action)
    {
        switch (action) {
        case MotionEvent::ACTION_NONE:
            return "ACTION_NONE";
        case MotionEvent::ACTION_POINTER_DOWN:
            return "ACTION_POINTER_DOWN";
        case MotionEvent::ACTION_POINTER_UP:
            return "ACTION_POINTER_UP";
        case MotionEvent::ACTION_DOWN:
            return "ACTION_DOWN";
        case MotionEvent::ACTION_UP:
            return "ACTION_UP";
        case MotionEvent::ACTION_CANCEL:
            return "ACTION_CANCEL";
        case MotionEvent::ACTION_MOVE:
            return "ACTION_MOVE";
        }
        return "";
    }

    gfx::RectF ClampBoundingBox(const gfx::RectF& bounds,
        float min_length,
        float max_length)
    {
        float width = bounds.width();
        float height = bounds.height();
        if (min_length) {
            width = std::max(min_length, width);
            height = std::max(min_length, height);
        }
        if (max_length) {
            width = std::min(max_length, width);
            height = std::min(max_length, height);
        }
        const gfx::PointF center = bounds.CenterPoint();
        return gfx::RectF(
            center.x() - width / 2.f, center.y() - height / 2.f, width, height);
    }

} // namespace

// GestureProvider:::Config

GestureProvider::Config::Config()
    : display(gfx::Display::kInvalidDisplayID, gfx::Rect(1, 1))
    , double_tap_support_for_platform_enabled(true)
    , gesture_begin_end_types_enabled(false)
    , min_gesture_bounds_length(0)
    , max_gesture_bounds_length(0)
{
}

GestureProvider::Config::~Config()
{
}

// GestureProvider::GestureListener

class GestureProvider::GestureListenerImpl : public ScaleGestureListener,
                                             public GestureListener,
                                             public DoubleTapListener {
public:
    GestureListenerImpl(const GestureProvider::Config& config,
        GestureProviderClient* client)
        : config_(config)
        , client_(client)
        , gesture_detector_(config.gesture_detector_config, this, this)
        , scale_gesture_detector_(config.scale_gesture_detector_config, this)
        , snap_scroll_controller_(config.gesture_detector_config.touch_slop,
              gfx::SizeF(config.display.size()))
        , ignore_multitouch_zoom_events_(false)
        , ignore_single_tap_(false)
        , pinch_event_sent_(false)
        , scroll_event_sent_(false)
        , max_diameter_before_show_press_(0)
        , show_press_event_sent_(false)
    {
    }

    void OnTouchEvent(const MotionEvent& event)
    {
        const bool in_scale_gesture = IsScaleGestureDetectionInProgress();
        snap_scroll_controller_.SetSnapScrollMode(event, in_scale_gesture);
        if (in_scale_gesture)
            SetIgnoreSingleTap(true);

        const MotionEvent::Action action = event.GetAction();
        if (action == MotionEvent::ACTION_DOWN) {
            current_down_time_ = event.GetEventTime();
            current_longpress_time_ = base::TimeTicks();
            ignore_single_tap_ = false;
            scroll_event_sent_ = false;
            pinch_event_sent_ = false;
            show_press_event_sent_ = false;
            gesture_detector_.set_longpress_enabled(true);
            tap_down_point_ = gfx::PointF(event.GetX(), event.GetY());
            max_diameter_before_show_press_ = event.GetTouchMajor();
        }

        gesture_detector_.OnTouchEvent(event);
        scale_gesture_detector_.OnTouchEvent(event);

        if (action == MotionEvent::ACTION_UP || action == MotionEvent::ACTION_CANCEL) {
            // Note: This call will have no effect if a fling was just generated, as
            // |Fling()| will have already signalled an end to touch-scrolling.
            if (scroll_event_sent_)
                Send(CreateGesture(ET_GESTURE_SCROLL_END, event));
            current_down_time_ = base::TimeTicks();
        } else if (action == MotionEvent::ACTION_MOVE) {
            if (!show_press_event_sent_ && !scroll_event_sent_) {
                max_diameter_before_show_press_ = std::max(max_diameter_before_show_press_, event.GetTouchMajor());
            }
        }
    }

    void Send(GestureEventData gesture)
    {
        DCHECK(!gesture.time.is_null());
        // The only valid events that should be sent without an active touch
        // sequence are SHOW_PRESS and TAP, potentially triggered by the double-tap
        // delay timing out.
        DCHECK(!current_down_time_.is_null() || gesture.type() == ET_GESTURE_TAP || gesture.type() == ET_GESTURE_SHOW_PRESS || gesture.type() == ET_GESTURE_BEGIN || gesture.type() == ET_GESTURE_END);

        if (gesture.primary_tool_type == MotionEvent::TOOL_TYPE_UNKNOWN || gesture.primary_tool_type == MotionEvent::TOOL_TYPE_FINGER) {
            gesture.details.set_bounding_box(
                ClampBoundingBox(gesture.details.bounding_box_f(),
                    config_.min_gesture_bounds_length,
                    config_.max_gesture_bounds_length));
        }

        switch (gesture.type()) {
        case ET_GESTURE_LONG_PRESS:
            DCHECK(!IsScaleGestureDetectionInProgress());
            current_longpress_time_ = gesture.time;
            break;
        case ET_GESTURE_LONG_TAP:
            current_longpress_time_ = base::TimeTicks();
            break;
        case ET_GESTURE_SCROLL_BEGIN:
            DCHECK(!scroll_event_sent_);
            scroll_event_sent_ = true;
            break;
        case ET_GESTURE_SCROLL_END:
            DCHECK(scroll_event_sent_);
            if (pinch_event_sent_)
                Send(GestureEventData(ET_GESTURE_PINCH_END, gesture));
            scroll_event_sent_ = false;
            break;
        case ET_SCROLL_FLING_START:
            DCHECK(scroll_event_sent_);
            scroll_event_sent_ = false;
            break;
        case ET_GESTURE_PINCH_BEGIN:
            DCHECK(!pinch_event_sent_);
            if (!scroll_event_sent_)
                Send(GestureEventData(ET_GESTURE_SCROLL_BEGIN, gesture));
            pinch_event_sent_ = true;
            break;
        case ET_GESTURE_PINCH_END:
            DCHECK(pinch_event_sent_);
            pinch_event_sent_ = false;
            break;
        case ET_GESTURE_SHOW_PRESS:
            // It's possible that a double-tap drag zoom (from ScaleGestureDetector)
            // will start before the press gesture fires (from GestureDetector), in
            // which case the press should simply be dropped.
            if (pinch_event_sent_ || scroll_event_sent_)
                return;
        default:
            break;
        };

        client_->OnGestureEvent(gesture);
        GestureTouchUMAHistogram::RecordGestureEvent(gesture);
    }

    // ScaleGestureListener implementation.
    bool OnScaleBegin(const ScaleGestureDetector& detector,
        const MotionEvent& e) override
    {
        if (ignore_multitouch_zoom_events_ && !detector.InAnchoredScaleMode())
            return false;
        return true;
    }

    void OnScaleEnd(const ScaleGestureDetector& detector,
        const MotionEvent& e) override
    {
        if (!pinch_event_sent_)
            return;
        Send(CreateGesture(ET_GESTURE_PINCH_END, e));
    }

    bool OnScale(const ScaleGestureDetector& detector,
        const MotionEvent& e) override
    {
        if (ignore_multitouch_zoom_events_ && !detector.InAnchoredScaleMode())
            return false;
        if (!pinch_event_sent_) {
            Send(CreateGesture(ET_GESTURE_PINCH_BEGIN,
                e.GetPointerId(),
                e.GetToolType(),
                detector.GetEventTime(),
                detector.GetFocusX(),
                detector.GetFocusY(),
                detector.GetFocusX() + e.GetRawOffsetX(),
                detector.GetFocusY() + e.GetRawOffsetY(),
                e.GetPointerCount(),
                GetBoundingBox(e, ET_GESTURE_PINCH_BEGIN),
                e.GetFlags()));
        }

        if (std::abs(detector.GetCurrentSpan() - detector.GetPreviousSpan()) < config_.scale_gesture_detector_config.min_pinch_update_span_delta) {
            return false;
        }

        float scale = detector.GetScaleFactor();
        if (scale == 1)
            return true;

        if (detector.InAnchoredScaleMode()) {
            // Relative changes in the double-tap scale factor computed by |detector|
            // diminish as the touch moves away from the original double-tap focus.
            // For historical reasons, Chrome has instead adopted a scale factor
            // computation that is invariant to the focal distance, where
            // the scale delta remains constant if the touch velocity is constant.
            float dy = (detector.GetCurrentSpanY() - detector.GetPreviousSpanY()) * 0.5f;
            scale = std::pow(scale > 1 ? 1.0f + kDoubleTapDragZoomSpeed
                                       : 1.0f - kDoubleTapDragZoomSpeed,
                std::abs(dy));
        }
        GestureEventDetails pinch_details(ET_GESTURE_PINCH_UPDATE);
        pinch_details.set_scale(scale);
        Send(CreateGesture(pinch_details,
            e.GetPointerId(),
            e.GetToolType(),
            detector.GetEventTime(),
            detector.GetFocusX(),
            detector.GetFocusY(),
            detector.GetFocusX() + e.GetRawOffsetX(),
            detector.GetFocusY() + e.GetRawOffsetY(),
            e.GetPointerCount(),
            GetBoundingBox(e, pinch_details.type()),
            e.GetFlags()));
        return true;
    }

    // GestureListener implementation.
    bool OnDown(const MotionEvent& e) override
    {
        GestureEventDetails tap_details(ET_GESTURE_TAP_DOWN);
        Send(CreateGesture(tap_details, e));

        // Return true to indicate that we want to handle touch.
        return true;
    }

    bool OnScroll(const MotionEvent& e1,
        const MotionEvent& e2,
        float raw_distance_x,
        float raw_distance_y) override
    {
        float distance_x = raw_distance_x;
        float distance_y = raw_distance_y;
        if (!scroll_event_sent_ && e2.GetPointerCount() == 1) {
            // Remove the touch slop region from the first scroll event to
            // avoid a jump. Touch slop isn't used for multi-finger
            // gestures, so in those cases we don't subtract the slop.
            float distance = std::sqrt(distance_x * distance_x + distance_y * distance_y);
            float epsilon = 1e-3f;
            if (distance > epsilon) {
                float ratio = std::max(0.f,
                                  distance - config_.gesture_detector_config.touch_slop)
                    / distance;
                distance_x *= ratio;
                distance_y *= ratio;
            }
        }

        snap_scroll_controller_.UpdateSnapScrollMode(distance_x, distance_y);
        if (snap_scroll_controller_.IsSnappingScrolls()) {
            if (snap_scroll_controller_.IsSnapHorizontal())
                distance_y = 0;
            else
                distance_x = 0;
        }

        if (!distance_x && !distance_y)
            return true;

        if (!scroll_event_sent_) {
            // Note that scroll start hints are in distance traveled, where
            // scroll deltas are in the opposite direction.
            GestureEventDetails scroll_details(
                ET_GESTURE_SCROLL_BEGIN, -raw_distance_x, -raw_distance_y);

            // Use the co-ordinates from the touch down, as these co-ordinates are
            // used to determine which layer the scroll should affect.
            Send(CreateGesture(scroll_details, e2.GetPointerId(), e2.GetToolType(),
                e2.GetEventTime(), e1.GetX(), e1.GetY(), e1.GetRawX(),
                e1.GetRawY(), e2.GetPointerCount(),
                GetBoundingBox(e2, scroll_details.type()),
                e2.GetFlags()));
            DCHECK(scroll_event_sent_);
        }

        GestureEventDetails scroll_details(ET_GESTURE_SCROLL_UPDATE, -distance_x,
            -distance_y);
        const gfx::RectF bounding_box = GetBoundingBox(e2, scroll_details.type());
        const gfx::PointF center = bounding_box.CenterPoint();
        const gfx::PointF raw_center = center + gfx::Vector2dF(e2.GetRawOffsetX(), e2.GetRawOffsetY());
        Send(CreateGesture(scroll_details, e2.GetPointerId(), e2.GetToolType(),
            e2.GetEventTime(), center.x(), center.y(),
            raw_center.x(), raw_center.y(), e2.GetPointerCount(),
            bounding_box, e2.GetFlags()));

        return true;
    }

    bool OnFling(const MotionEvent& e1,
        const MotionEvent& e2,
        float velocity_x,
        float velocity_y) override
    {
        if (snap_scroll_controller_.IsSnappingScrolls()) {
            if (snap_scroll_controller_.IsSnapHorizontal()) {
                velocity_y = 0;
            } else {
                velocity_x = 0;
            }
        }

        if (!velocity_x && !velocity_y)
            return true;

        if (!scroll_event_sent_) {
            // The native side needs a ET_GESTURE_SCROLL_BEGIN before
            // ET_SCROLL_FLING_START to send the fling to the correct target.
            // The distance traveled in one second is a reasonable scroll start hint.
            GestureEventDetails scroll_details(
                ET_GESTURE_SCROLL_BEGIN, velocity_x, velocity_y);
            Send(CreateGesture(scroll_details, e2));
        }

        GestureEventDetails fling_details(
            ET_SCROLL_FLING_START, velocity_x, velocity_y);
        Send(CreateGesture(fling_details, e2));
        return true;
    }

    bool OnSwipe(const MotionEvent& e1,
        const MotionEvent& e2,
        float velocity_x,
        float velocity_y) override
    {
        GestureEventDetails swipe_details(ET_GESTURE_SWIPE, velocity_x, velocity_y);
        Send(CreateGesture(swipe_details, e2));
        return true;
    }

    bool OnTwoFingerTap(const MotionEvent& e1, const MotionEvent& e2) override
    {
        // The location of the two finger tap event should be the location of the
        // primary pointer.
        GestureEventDetails two_finger_tap_details(
            ET_GESTURE_TWO_FINGER_TAP, e1.GetTouchMajor(), e1.GetTouchMajor());
        Send(CreateGesture(two_finger_tap_details,
            e2.GetPointerId(),
            e2.GetToolType(),
            e2.GetEventTime(),
            e1.GetX(),
            e1.GetY(),
            e1.GetRawX(),
            e1.GetRawY(),
            e2.GetPointerCount(),
            GetBoundingBox(e2, two_finger_tap_details.type()),
            e2.GetFlags()));
        return true;
    }

    void OnShowPress(const MotionEvent& e) override
    {
        GestureEventDetails show_press_details(ET_GESTURE_SHOW_PRESS);
        show_press_event_sent_ = true;
        Send(CreateGesture(show_press_details, e));
    }

    bool OnSingleTapUp(const MotionEvent& e, int tap_count) override
    {
        // This is a hack to address the issue where user hovers
        // over a link for longer than double_tap_timeout_, then
        // OnSingleTapConfirmed() is not triggered. But we still
        // want to trigger the tap event at UP. So we override
        // OnSingleTapUp() in this case. This assumes singleTapUp
        // gets always called before singleTapConfirmed.
        if (!ignore_single_tap_) {
            if (e.GetEventTime() - current_down_time_ > config_.gesture_detector_config.double_tap_timeout) {
                return OnSingleTapImpl(e, tap_count);
            } else if (!IsDoubleTapEnabled()) {
                // If double-tap has been disabled, there is no need to wait
                // for the double-tap timeout.
                return OnSingleTapImpl(e, tap_count);
            } else {
                // Notify Blink about this tapUp event anyway, when none of the above
                // conditions applied.
                Send(CreateTapGesture(ET_GESTURE_TAP_UNCONFIRMED, e, 1));
            }
        }

        if (e.GetAction() == MotionEvent::ACTION_UP && !current_longpress_time_.is_null() && !IsScaleGestureDetectionInProgress()) {
            GestureEventDetails long_tap_details(ET_GESTURE_LONG_TAP);
            Send(CreateGesture(long_tap_details, e));
            return true;
        }

        return false;
    }

    // DoubleTapListener implementation.
    bool OnSingleTapConfirmed(const MotionEvent& e) override
    {
        return OnSingleTapImpl(e, 1);
    }

    bool OnDoubleTap(const MotionEvent& e) override
    {
        return scale_gesture_detector_.OnDoubleTap(e);
    }

    bool OnDoubleTapEvent(const MotionEvent& e) override
    {
        switch (e.GetAction()) {
        case MotionEvent::ACTION_DOWN:
            gesture_detector_.set_longpress_enabled(false);
            break;

        case MotionEvent::ACTION_UP:
            if (!IsPinchInProgress() && !IsScrollInProgress()) {
                Send(CreateTapGesture(ET_GESTURE_DOUBLE_TAP, e, 1));
                return true;
            }
            break;

        default:
            break;
        }
        return false;
    }

    void OnLongPress(const MotionEvent& e) override
    {
        DCHECK(!IsDoubleTapInProgress());
        SetIgnoreSingleTap(true);
        GestureEventDetails long_press_details(ET_GESTURE_LONG_PRESS);
        Send(CreateGesture(long_press_details, e));
    }

    GestureEventData CreateGesture(const GestureEventDetails& details,
        int motion_event_id,
        MotionEvent::ToolType primary_tool_type,
        base::TimeTicks time,
        float x,
        float y,
        float raw_x,
        float raw_y,
        size_t touch_point_count,
        const gfx::RectF& bounding_box,
        int flags) const
    {
        return GestureEventData(details,
            motion_event_id,
            primary_tool_type,
            time,
            x,
            y,
            raw_x,
            raw_y,
            touch_point_count,
            bounding_box,
            flags);
    }

    GestureEventData CreateGesture(EventType type,
        int motion_event_id,
        MotionEvent::ToolType primary_tool_type,
        base::TimeTicks time,
        float x,
        float y,
        float raw_x,
        float raw_y,
        size_t touch_point_count,
        const gfx::RectF& bounding_box,
        int flags) const
    {
        return GestureEventData(GestureEventDetails(type),
            motion_event_id,
            primary_tool_type,
            time,
            x,
            y,
            raw_x,
            raw_y,
            touch_point_count,
            bounding_box,
            flags);
    }

    GestureEventData CreateGesture(const GestureEventDetails& details,
        const MotionEvent& event) const
    {
        return GestureEventData(details,
            event.GetPointerId(),
            event.GetToolType(),
            event.GetEventTime(),
            event.GetX(),
            event.GetY(),
            event.GetRawX(),
            event.GetRawY(),
            event.GetPointerCount(),
            GetBoundingBox(event, details.type()),
            event.GetFlags());
    }

    GestureEventData CreateGesture(EventType type,
        const MotionEvent& event) const
    {
        return CreateGesture(GestureEventDetails(type), event);
    }

    GestureEventData CreateTapGesture(EventType type,
        const MotionEvent& event,
        int tap_count) const
    {
        DCHECK_GE(tap_count, 0);
        GestureEventDetails details(type);
        details.set_tap_count(tap_count);
        return CreateGesture(details, event);
    }

    gfx::RectF GetBoundingBox(const MotionEvent& event, EventType type) const
    {
        // Can't use gfx::RectF::Union, as it ignores touches with a radius of 0.
        float left = std::numeric_limits<float>::max();
        float top = std::numeric_limits<float>::max();
        float right = -std::numeric_limits<float>::max();
        float bottom = -std::numeric_limits<float>::max();
        for (size_t i = 0; i < event.GetPointerCount(); ++i) {
            float x, y, diameter;
            // Only for the show press and tap events, the bounding box is calculated
            // based on the touch start point and the maximum diameter before the
            // show press event is sent.
            if (type == ET_GESTURE_SHOW_PRESS || type == ET_GESTURE_TAP || type == ET_GESTURE_TAP_UNCONFIRMED) {
                DCHECK_EQ(0U, i);
                diameter = max_diameter_before_show_press_;
                x = tap_down_point_.x();
                y = tap_down_point_.y();
            } else {
                diameter = event.GetTouchMajor(i);
                x = event.GetX(i);
                y = event.GetY(i);
            }
            x = x - diameter / 2;
            y = y - diameter / 2;
            left = std::min(left, x);
            right = std::max(right, x + diameter);
            top = std::min(top, y);
            bottom = std::max(bottom, y + diameter);
        }
        return gfx::RectF(left, top, right - left, bottom - top);
    }

    void SetDoubleTapEnabled(bool enabled)
    {
        DCHECK(!IsDoubleTapInProgress());
        gesture_detector_.SetDoubleTapListener(enabled ? this : NULL);
    }

    void SetMultiTouchZoomEnabled(bool enabled)
    {
        // Note that returning false from |OnScaleBegin()| or |OnScale()| prevents
        // the detector from emitting further scale updates for the current touch
        // sequence. Thus, if multitouch events are enabled in the middle of a
        // gesture, it will only take effect with the next gesture.
        ignore_multitouch_zoom_events_ = !enabled;
    }

    bool IsDoubleTapInProgress() const
    {
        return gesture_detector_.is_double_tapping() || (IsScaleGestureDetectionInProgress() && InAnchoredScaleMode());
    }

    bool IsScrollInProgress() const { return scroll_event_sent_; }

    bool IsPinchInProgress() const { return pinch_event_sent_; }

private:
    bool OnSingleTapImpl(const MotionEvent& e, int tap_count)
    {
        // Long taps in the edges of the screen have their events delayed by
        // ContentViewHolder for tab swipe operations. As a consequence of the delay
        // this method might be called after receiving the up event.
        // These corner cases should be ignored.
        if (ignore_single_tap_)
            return true;

        ignore_single_tap_ = true;

        Send(CreateTapGesture(ET_GESTURE_TAP, e, tap_count));
        return true;
    }

    bool IsScaleGestureDetectionInProgress() const
    {
        return scale_gesture_detector_.IsInProgress();
    }

    bool InAnchoredScaleMode() const
    {
        return scale_gesture_detector_.InAnchoredScaleMode();
    }

    bool IsDoubleTapEnabled() const
    {
        return gesture_detector_.has_doubletap_listener();
    }

    void SetIgnoreSingleTap(bool value) { ignore_single_tap_ = value; }

    const GestureProvider::Config config_;
    GestureProviderClient* const client_;

    GestureDetector gesture_detector_;
    ScaleGestureDetector scale_gesture_detector_;
    SnapScrollController snap_scroll_controller_;

    base::TimeTicks current_down_time_;

    // Keeps track of the current GESTURE_LONG_PRESS event. If a context menu is
    // opened after a GESTURE_LONG_PRESS, this is used to insert a
    // GESTURE_TAP_CANCEL for removing any ::active styling.
    base::TimeTicks current_longpress_time_;

    // Completely silence multi-touch (pinch) scaling events. Used in WebView when
    // zoom support is turned off.
    bool ignore_multitouch_zoom_events_;

    // TODO(klobag): This is to avoid a bug in GestureDetector. With multi-touch,
    // always_in_tap_region_ is not reset. So when the last finger is up,
    // |OnSingleTapUp()| will be mistakenly fired.
    bool ignore_single_tap_;

    // Tracks whether {PINCH|SCROLL}_BEGIN events have been forwarded for the
    // current touch sequence.
    bool pinch_event_sent_;
    bool scroll_event_sent_;

    // Only track the maximum diameter before the show press event has been
    // sent and a tap must still be possible for this touch sequence.
    float max_diameter_before_show_press_;

    gfx::PointF tap_down_point_;

    // Tracks whether an ET_GESTURE_SHOW_PRESS event has been sent for this touch
    // sequence.
    bool show_press_event_sent_;

    DISALLOW_COPY_AND_ASSIGN(GestureListenerImpl);
};

// GestureProvider

GestureProvider::GestureProvider(const Config& config,
    GestureProviderClient* client)
    : double_tap_support_for_page_(true)
    , double_tap_support_for_platform_(
          config.double_tap_support_for_platform_enabled)
    , gesture_begin_end_types_enabled_(config.gesture_begin_end_types_enabled)
{
    DCHECK(client);
    DCHECK(!config.min_gesture_bounds_length || !config.max_gesture_bounds_length || config.min_gesture_bounds_length <= config.max_gesture_bounds_length);
    TRACE_EVENT0("input", "GestureProvider::InitGestureDetectors");
    gesture_listener_.reset(new GestureListenerImpl(config, client));
    UpdateDoubleTapDetectionSupport();
}

GestureProvider::~GestureProvider()
{
}

bool GestureProvider::OnTouchEvent(const MotionEvent& event)
{
    TRACE_EVENT1("input",
        "GestureProvider::OnTouchEvent",
        "action",
        GetMotionEventActionName(event.GetAction()));

    DCHECK_NE(0u, event.GetPointerCount());

    if (!CanHandle(event))
        return false;

    OnTouchEventHandlingBegin(event);
    gesture_listener_->OnTouchEvent(event);
    OnTouchEventHandlingEnd(event);
    uma_histogram_.RecordTouchEvent(event);
    return true;
}

void GestureProvider::ResetDetection()
{
    MotionEventGeneric generic_cancel_event(MotionEvent::ACTION_CANCEL,
        base::TimeTicks::Now(),
        PointerProperties());
    OnTouchEvent(generic_cancel_event);
}

void GestureProvider::SetMultiTouchZoomSupportEnabled(bool enabled)
{
    gesture_listener_->SetMultiTouchZoomEnabled(enabled);
}

void GestureProvider::SetDoubleTapSupportForPlatformEnabled(bool enabled)
{
    if (double_tap_support_for_platform_ == enabled)
        return;
    double_tap_support_for_platform_ = enabled;
    UpdateDoubleTapDetectionSupport();
}

void GestureProvider::SetDoubleTapSupportForPageEnabled(bool enabled)
{
    if (double_tap_support_for_page_ == enabled)
        return;
    double_tap_support_for_page_ = enabled;
    UpdateDoubleTapDetectionSupport();
}

bool GestureProvider::IsScrollInProgress() const
{
    return gesture_listener_->IsScrollInProgress();
}

bool GestureProvider::IsPinchInProgress() const
{
    return gesture_listener_->IsPinchInProgress();
}

bool GestureProvider::IsDoubleTapInProgress() const
{
    return gesture_listener_->IsDoubleTapInProgress();
}

bool GestureProvider::CanHandle(const MotionEvent& event) const
{
    // Aura requires one cancel event per touch point, whereas Android requires
    // one cancel event per touch sequence. Thus we need to allow extra cancel
    // events.
    return current_down_event_ || event.GetAction() == MotionEvent::ACTION_DOWN || event.GetAction() == MotionEvent::ACTION_CANCEL;
}

void GestureProvider::OnTouchEventHandlingBegin(const MotionEvent& event)
{
    switch (event.GetAction()) {
    case MotionEvent::ACTION_DOWN:
        current_down_event_ = event.Clone();
        if (gesture_begin_end_types_enabled_)
            gesture_listener_->Send(
                gesture_listener_->CreateGesture(ET_GESTURE_BEGIN, event));
        break;
    case MotionEvent::ACTION_POINTER_DOWN:
        if (gesture_begin_end_types_enabled_) {
            const int action_index = event.GetActionIndex();
            gesture_listener_->Send(gesture_listener_->CreateGesture(
                ET_GESTURE_BEGIN,
                event.GetPointerId(),
                event.GetToolType(),
                event.GetEventTime(),
                event.GetX(action_index),
                event.GetY(action_index),
                event.GetRawX(action_index),
                event.GetRawY(action_index),
                event.GetPointerCount(),
                gesture_listener_->GetBoundingBox(event, ET_GESTURE_BEGIN),
                event.GetFlags()));
        }
        break;
    case MotionEvent::ACTION_POINTER_UP:
    case MotionEvent::ACTION_UP:
    case MotionEvent::ACTION_CANCEL:
    case MotionEvent::ACTION_MOVE:
        break;
    case MotionEvent::ACTION_NONE:
        NOTREACHED();
        break;
    }
}

void GestureProvider::OnTouchEventHandlingEnd(const MotionEvent& event)
{
    switch (event.GetAction()) {
    case MotionEvent::ACTION_UP:
    case MotionEvent::ACTION_CANCEL: {
        if (gesture_begin_end_types_enabled_)
            gesture_listener_->Send(
                gesture_listener_->CreateGesture(ET_GESTURE_END, event));

        current_down_event_.reset();

        UpdateDoubleTapDetectionSupport();
        break;
    }
    case MotionEvent::ACTION_POINTER_UP:
        if (gesture_begin_end_types_enabled_)
            gesture_listener_->Send(
                gesture_listener_->CreateGesture(ET_GESTURE_END, event));
        break;
    case MotionEvent::ACTION_DOWN:
    case MotionEvent::ACTION_POINTER_DOWN:
    case MotionEvent::ACTION_MOVE:
        break;
    case MotionEvent::ACTION_NONE:
        NOTREACHED();
        break;
    }
}

void GestureProvider::UpdateDoubleTapDetectionSupport()
{
    // The GestureDetector requires that any provided DoubleTapListener remain
    // attached to it for the duration of a touch sequence. Defer any potential
    // null'ing of the listener until the sequence has ended.
    if (current_down_event_)
        return;

    const bool double_tap_enabled = double_tap_support_for_page_ && double_tap_support_for_platform_;
    gesture_listener_->SetDoubleTapEnabled(double_tap_enabled);
}

} //  namespace ui
